#!/usr/bin/env python
#
# Copyright 2009 Facebook
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

# Changes made by Alden Torres
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# - Comment import ioloop.
# - Removed dependency from pycurl
# - Rewrite of HTTPClient, AsyncHTTPClient

"""Blocking and non-blocking HTTP client implementations using pycurl."""

import calendar
import collections
import cStringIO
import email.utils
import errno
import functools
import httplib
#import ioloop
import logging
import time

from System import *
from System.IO import *
from System.IO.Compression import *
from System.Net import *
from System.Text import *

class HTTPClient(object):
    """A blocking HTTP client backed with pycurl.

    Typical usage looks like this:

        http_client = httpclient.HTTPClient()
        try:
            response = http_client.fetch("http://www.google.com/")
            print response.body
        except Exception, e:
            print "Error:", e

    fetch() can take a string URL or an HTTPRequest instance, which offers
    more options, like executing POST/PUT/DELETE requests.
    """
    def fetch(self, request, **kwargs):
        """Executes an HTTPRequest, returning an HTTPResponse.

        If an error occurs during the fetch, we raise an HTTPError.
        """
        if not isinstance(request, HTTPRequest):
           request = HTTPRequest(url=request, **kwargs)
        headers = {}
        try:
        	code = -1
        	webRequest = _setup_request(request, headers)
        	webResponse = webRequest.GetResponse()
        	stream = webResponse.GetResponseStream()
        	if webResponse.ContentEncoding.ToLower().Contains("gzip"):
        		stream = GZipStream(stream, CompressionMode.Decompress)
        	elif webResponse.ContentEncoding.ToLower().Contains("deflate"):
        		stream = DeflateStream(stream, CompressionMode.Decompress)
        	stream = StreamReader(stream, Encoding.UTF8)
        	body = stream.ReadToEnd()
        	stream.Close()
        	webResponse.Close()
        	
        	code = int(webResponse.StatusCode)
        	if code < 200 or code >= 300:
        		raise HTTPError(code)
        	effective_url = webResponse.ResponseUri
        	return HTTPResponse(
        		request=request, code=code, headers=headers,
        		body=body, effective_url=effective_url)
        except Exception, e:
        	raise HTTPError(code)


class AsyncHTTPClient(object):
    """An non-blocking HTTP client backed with pycurl.

    Example usage:

        import ioloop

        def handle_request(response):
            if response.error:
                print "Error:", response.error
            else:
                print response.body

        http_client = httpclient.AsyncHTTPClient()
        http_client.fetch("http://www.google.com/", handle_request)

    fetch() can take a string URL or an HTTPRequest instance, which offers
    more options, like executing POST/PUT/DELETE requests.

    The keyword argument max_clients to the AsyncHTTPClient constructor
    determines the maximum number of simultaneous fetch() operations that
    can execute in parallel on each IOLoop.
    """
    def fetch(self, request, callback, **kwargs):
        """Executes an HTTPRequest, calling callback with an HTTPResponse.

        If an error occurs during the fetch, the HTTPResponse given to the
        callback has a non-None error attribute that contains the exception
        encountered during the request. You can call response.reraise() to
        throw the exception (if any) in the callback.
        """
        if not isinstance(request, HTTPRequest):
           request = HTTPRequest(url=request, **kwargs)
           
        self._request = request
        self._headers = {}
        self._callback = callback
        self._start_time = time.time()
        webRequest = _setup_request(self._request, self._headers)
        webRequest.BeginGetResponse(AsyncCallback(self._response_callback), webRequest)
        
    def _response_callback(self, asyncResult):
		try:
			webRequest = asyncResult.AsyncState
			webResponse = webRequest.EndGetResponse(asyncResult)
			stream = webResponse.GetResponseStream()
			if webResponse.ContentEncoding.ToLower().Contains("gzip"):
				stream = GZipStream(stream, CompressionMode.Decompress)
			elif webResponse.ContentEncoding.ToLower().Contains("deflate"):
				stream = DeflateStream(stream, CompressionMode.Decompress)        
			stream = StreamReader(stream, Encoding.UTF8)
			body = stream.ReadToEnd()
			stream.Close()
			webResponse.Close()
			
			code = int(webResponse.StatusCode)
			effective_url = webResponse.ResponseUri
			self._callback(HTTPResponse(
				request=self._request, code=code, headers=self._headers,
				body=body, effective_url=effective_url,
				request_time=time.time() - self._start_time))
				
		except Exception, e:
			print e
			self._callback(HTTPResponse(request=self._request, error = e))


class HTTPRequest(object):
    def __init__(self, url, method="GET", headers={}, body=None,
                 auth_username=None, auth_password=None,
                 connect_timeout=None, request_timeout=None,
                 if_modified_since=None, follow_redirects=True,
                 max_redirects=5, user_agent=None, use_gzip=True,
                 network_interface=None, streaming_callback=None):
        if if_modified_since:
            timestamp = calendar.timegm(if_modified_since.utctimetuple())
            headers["If-Modified-Since"] = email.utils.formatdate(
                timestamp, localtime=False, usegmt=True)
        if "Pragma" not in headers:
            headers["Pragma"] = ""
        self.url = _utf8(url)
        self.method = method
        self.headers = headers
        self.body = body
        self.auth_username = _utf8(auth_username)
        self.auth_password = _utf8(auth_password)
        self.connect_timeout = connect_timeout or 20.0
        self.request_timeout = request_timeout or 20.0
        self.follow_redirects = follow_redirects
        self.max_redirects = max_redirects
        self.user_agent = user_agent
        self.use_gzip = use_gzip
        self.network_interface = network_interface
        self.streaming_callback = streaming_callback


class HTTPResponse(object):
    def __init__(self, request, code, headers={}, body="", effective_url=None,
                 error=None, request_time=None):
		self.request = request
		self.code = code
		self.headers = headers
		self.body = body
		if effective_url is None:
			self.effective_url = request.url
		else:
			self.effective_url = effective_url
		if error is None:
			if self.code < 200 or self.code >= 300:
				self.error = HTTPError(self.code)
			else:
				self.error = None
		else:
			self.error = error
		self.request_time = request_time

    def rethrow(self):
        if self.error:
            raise self.error

    def __repr__(self):
        args = ",".join("%s=%r" % i for i in self.__dict__.iteritems())
        return "%s(%s)" % (self.__class__.__name__, args)


class HTTPError(Exception):
    def __init__(self, code, message=None):
        self.code = code
        message = message or httplib.responses.get(code, "Unknown")
        Exception.__init__(self, "HTTP %d: %s" % (self.code, message))
        
        
def _setup_request(request, headers):
    webRequest = WebRequest.Create(request.url)
    
    for i in request.headers.iteritems():
    	webRequest.Headers[i[0]] = i[1]
    
    webRequest.AllowAutoRedirect = request.follow_redirects
    webRequest.MaximumAutomaticRedirections = int(request.max_redirects)
    webRequest.Timeout = int(request.connect_timeout) * 1000
    #curl.setopt(pycurl.CONNECTTIMEOUT, int(request.connect_timeout))
    #curl.setopt(pycurl.TIMEOUT, int(request.request_timeout))
    if request.user_agent:
    	webRequest.UserAgent = request.user_agent
    else:
        webRequest.UserAgent = "Mozilla/4.0 (compatible)"
    #if request.network_interface:
    #    curl.setopt(pycurl.INTERFACE, request.network_interface)
    if request.use_gzip:
        webRequest.Headers["Accept-Encoding"] = "gzip,deflate"
    else:
        webRequest.Headers["Accept-Encoding"] = "none"
        
    webRequest.Method = request.method

    # Handle curl's cryptic options for every individual HTTP method
    if request.method in ("POST", "PUT"):
    	webRequest.ContentLength = len(request.body)
    	stream = StreamWriter(response.GetRequestStream(), Encoding.UTF8)
    	stream.Write(request.body);
    	stream.Close()

    if request.auth_username and request.auth_password:
        userpwd = "%s:%s" % (request.auth_username, request.auth_password)
        webRequest.Headers.Add("Authorization", "Basic " + Convert.ToBase64String(Encoding.ASCII.GetBytes(userpwd)))
        logging.info("%s %s (username: %r)", request.method, request.url,
                     request.auth_username)
    else:
        logging.info("%s %s", request.method, request.url)
        
	return webRequest


def _utf8(value):
    if value is None:
        return value
    if isinstance(value, unicode):
        return value.encode("utf-8")
    assert isinstance(value, str)
    return value